﻿#region License Information (GPL v2)
/*
    Source Queries - Source Server Queries
    Copyright (C) 2009  Source Queries Developers

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License
    as published by the Free Software Foundation; either version 2
    of the License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
    
    Optionally you can also view the license at <http://www.gnu.org/licenses/>.
*/
#endregion

using System;
using System.ComponentModel;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace SourceQueries
{
    public class Source : IDisposable
    {
        public delegate void StringEventHandler(string text);
        public delegate void LogEventHandler(LogPacket packet);

        public event StringEventHandler ConnectionStatus;
        public event StringEventHandler ServerOutput;
        public event LogEventHandler LogOutput;

        public bool Connected;
        public string Password;

        private Socket socket, udp;
        private IPEndPoint connection;
        private bool listening;

        public bool Connect(string server, string password)
        {
            string[] split = server.Split(':');
            if (split.Length == 2)
            {
                int port;
                if (int.TryParse(split[1], out port))
                {
                    return Connect(split[0], port, password);
                }
            }
            return false;
        }

        public bool Connect(string ip, int port, string password)
        {
            return Connect(new IPEndPoint(IPAddress.Parse(ip), port), password);
        }

        public bool Connect(IPEndPoint ipep, string password)
        {
            connection = ipep;
            Password = password;
            if (ConnectionStatus != null) ConnectionStatus(string.Format("Connecting to {0}...", connection.ToString()));
            try
            {
                socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                socket.Connect(connection);
            }
            catch (Exception e)
            {
#if DEBUG
                Console.WriteLine(e.ToString());
#endif
                OnConnectionStatus("Connection failed.");
                return false;
            }

            ConnectAuth();

            return true;
        }

        private void OnConnectionStatus(string text)
        {
            if (ConnectionStatus != null)
            {
                ConnectionStatus(text);
            }
        }

        private void OnServerOutput(string text)
        {
            if (ServerOutput != null)
            {
                ServerOutput(text);
            }
        }

        public void ListenLogs(int port)
        {
            udp = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            EndPoint ep = new IPEndPoint(IPAddress.Any, port);
            udp.Bind(ep);
            listening = true;
            BackgroundWorker bw = new BackgroundWorker { WorkerReportsProgress = true };
            bw.DoWork += new DoWorkEventHandler(ListenLogs_DoWork);
            bw.ProgressChanged += new ProgressChangedEventHandler(ListLogs_ProgressChanged);
            bw.RunWorkerAsync(bw);
        }

        private void ListenLogs_DoWork(object sender, DoWorkEventArgs e)
        {
            while (listening)
            {
                byte[] buffer = new byte[1400];
                int length = udp.Receive(buffer);
                if (buffer[4] == 0x52)
                {
                    ((BackgroundWorker)e.Argument).ReportProgress(length, buffer.ToString(5, length - 6));
                }
                else
                {
                    throw new Exception("This is not log packet. " + buffer.ToHex());
                }
            }
        }

        private void ListLogs_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            if (LogOutput != null)
            {
                LogPacket packet = new LogPacket((string)e.UserState);
                LogOutput(packet);
            }
        }

        public void ConnectAuth()
        {
            ConnectAuth(Password);
        }

        public void ConnectAuth(string password)
        {
            RCONPacket packet = new RCONPacket();
            packet.Command = RCONPacket.ServerDataSend.SERVERDATA_AUTH;
            packet.String1 = password;

            SendPacket(packet);
            ReceivePackets();
        }

        public void SendCommand(string command)
        {
            if (Connected)
            {
                RCONPacket packet = new RCONPacket();
                packet.Command = RCONPacket.ServerDataSend.SERVERDATA_EXECCOMMAND;
                packet.String1 = command;

                SendPacket(packet);
            }
        }

        public void SendPacket(RCONPacket packet)
        {
            byte[] bytes = packet.GetBytes();
            socket.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, new AsyncCallback(x => socket.EndSend(x)), this);
        }

        public void ReceivePackets()
        {
            StreamData stream = new StreamData();
            stream.Buffer = new byte[4];
            ReceivePacket(stream);
        }

        private void ReceivePacket(StreamData stream)
        {
            socket.BeginReceive(stream.Buffer, 0, stream.Buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), stream);
        }

        private void ReceiveCallback(IAsyncResult result)
        {
            try
            {
                StreamData stream = (StreamData)result.AsyncState;
                stream.Position += socket.EndReceive(result);

                //Console.WriteLine("Length: {0}\r\nData: {1}", stream.Data.Length, stream.Data.ToHex());

                if (stream.Length == 0 && stream.Buffer.Length == 4)
                {
                    stream.Length = stream.Buffer.ToInt32();
                    if (stream.Length == 0)
                    {
                        Reconnect();
                        return;
                    }

                    stream.Position = 0;
                    stream.Buffer = new byte[stream.Length];
                    ReceivePacket(stream);
                }
                else
                {
                    if (stream.Position < stream.Length)
                    {
                        socket.BeginReceive(stream.Buffer, stream.Position, stream.Length - stream.Position,
                            SocketFlags.None, new AsyncCallback(ReceiveCallback), stream);
                    }
                    else
                    {
                        if (stream.Buffer.Length > 9)
                        {
                            ParsePacket(stream.Buffer);
                        }

                        ReceivePackets();
                    }
                }
            }
            catch
            {
                Reconnect();
            }
        }

        public void Dispose()
        {
            Disconnect();

            if (udp != null)
            {
                udp.Close();
            }
        }

        public void Disconnect()
        {
            if (socket != null)
            {
                socket.Close();
            }
        }

        public void Reconnect()
        {
            for (int i = 0; i < 3; i++)
            {
                Disconnect();

                if (Connect(connection, Password))
                {
                    break;
                }

                Thread.Sleep(2500);
            }
        }

        private void ParsePacket(byte[] bytes)
        {
            RCONPacket packet = RCONPacket.Parse(bytes);

            /*Console.WriteLine("Response Size: '{0}' - Request ID: '{1}' - Server Data: '{2}' - String1: '{3}' - String2: '{4}'",
                bytes.Length, packet.RequestID, packet.Response.ToString(), packet.String1, packet.String2);*/

            switch (packet.Response)
            {
                case RCONPacket.ServerDataReceive.SERVERDATA_AUTH_RESPONSE:
                    if (packet.RequestID == -1)
                    {
                        if (ConnectionStatus != null) ConnectionStatus("Password wrong.");
                    }
                    else
                    {
                        Connected = true;
                        if (ConnectionStatus != null) ConnectionStatus("Connection succeeded.");
                    }
                    break;
                case RCONPacket.ServerDataReceive.SERVERDATA_RESPONSE_VALUE:
                    if (string.IsNullOrEmpty(packet.String1))
                    {
#if DEBUG
                        Console.WriteLine("Junk packet.");
#endif
                    }
                    else
                    {
                        OnServerOutput(packet.String1);
                    }
                    break;
            }
        }

        public class StreamData
        {
            public byte[] Buffer;
            public int Length;
            public int Position;
        }
    }
}